(function($){
  //Common variables used
  var map;
  var inEditionMode = false;
  var polyline = [];
  var markers = [];
  var pathMarkers = [];
  var infoWindow;
  var createPointListener;
  var createStopListener;
  var opts;
  var directionsService;
  
  //Based on a given origin and destination call google maps service to create a route for DRIVING directions.
  function getDirectionsFromGoogle(origin, destination, insertAtEnd, insertAtIndex) {
    var request = {
      origin: origin,
      destination: destination,
      travelMode: google.maps.DirectionsTravelMode.DRIVING
    };
    
    if (insertAtIndex == undefined) {
      insertAtIndex = 0;
    }
    
    directionsService.route(request, function(response, status) {
      if (status == google.maps.DirectionsStatus.OK) {
        
        if (response.routes && response.routes.length > 0){
          route = response.routes[0];
          
          if (route.overview_path && route.overview_path.length > 0){
            $.each(route.overview_path, function(index, point){
              if (insertAtEnd) {
                polyline.getPath().push(point);
                createCustomMarker(point, {setMap: true});
              }
              else {
                polyline.getPath().insertAt(insertAtIndex, point);
                createCustomMarker(point, {setMap: true}, insertAtIndex);
              }
            });
            
          }
        }
      }
    });
    
  }
  
  //Convert an Google Maps array of LatLng to the normal form like {lat: '-22.0', lng: '-44.0'} so,
  //handlers can manipulate it correctly.
  function convertPositionsToNormalForm(array) {
    var convertedArray = [];
    
    $.each(array, function(index, item) {
      convertedArray.push({
                            lat: item.getPosition().lat(),
                            lng: item.getPosition().lng()
                          });
    });
    return convertedArray
  }
  
  //Check where is the user attemp to insert the item on the map, whether it's closest 
  //to end or the beggining of the array.
  function insertAtEnd(point, firstItem, lastItem) {
    var distanceFromBeggining = google.maps.geometry.spherical.computeDistanceBetween (point.latLng, firstItem);
    var distanceFromEnd = google.maps.geometry.spherical.computeDistanceBetween (point.latLng, lastItem)
    //Compare the current point being set against the beginning and the end of the polyline to check
    //whether the point is closest to the begin or to the end.
    if (distanceFromBeggining < distanceFromEnd) {
      return false;
    }
    else {
      return true;
    }
  }
  
  //Create a point on the map while editing a route. This point will be actually a marker
  //as they are used to create new points on the map.
  function createPoint(point) {
    var polylineStart = polyline.getPath().getAt(0);
    var polylineEnd = polyline.getPath().getAt(polyline.getPath().length - 1);
    var positionInsertAt = insertAtEnd(point, polylineStart, polylineEnd);
    
    if (directionsService == undefined) {
      directionsService = new google.maps.DirectionsService();
    }
    
    if (positionInsertAt) {
      getDirectionsFromGoogle(polylineEnd, point.latLng, true);
    }
    else {
      getDirectionsFromGoogle(polylineStart, point.latLng, false);
    }
    console.log(pathMarkers);
  }
  
  //Create a stop on the map while editing a route.
  function createStop(point) {
    var marker = createSimpleMarker({
                          setMap: true, 
                          lat: point.latLng.lat(), 
                          lng: point.latLng.lng(),
                          desc: ''
                        });
    markers.push(marker);
  }
  
  //Get bounds for a given path
  function getBoundsForPath(path) {
    var bounds = new google.maps.LatLngBounds();
    for (var i = 0; i < path.getLength(); i++) {
       bounds.extend(path.getAt(i));
    }
    return bounds;
  }
  
  //Set a polyline on the given map. For routePath is expected an array of objects in the 
  //following format {lat: '-22.0', lng: '-44.0'}
  function createPolyline(routePath, polylineOptions){
    
    var defaultPolylineOptions = {
      clickable:      false,
      strokeColor:    '#5E9DC8',
      strokeOpacity:  0.75,
      strokeWeight:   6
    }
    defaultPolylineOptions = $.extend({}, defaultPolylineOptions, polylineOptions);
    
    var path = [];
    $.each(routePath, function(index, element) {
      var pointPosition = new google.maps.LatLng(parseFloat(element.lat),parseFloat(element.lng));
      path.push(pointPosition);
      if (inEditionMode) {
        createCustomMarker(pointPosition);
      }
    });
    defaultPolylineOptions.path = path;
    polyline = new google.maps.Polyline(defaultPolylineOptions);
    polyline.setMap(map);
    return polyline;
  }
  
  //Set all stops of a given path. For this is expected an array of objects in the
  //following format {lat: '-22.0', lng: '-44.0', desc: 'Stop description'}
  function createStops(stops, setMap) {
    $.each(stops, function(index, stop){
      //TODO Improve this is not cool.
      stop.setMap = setMap;
      markers.push(createSimpleMarker(stop));
    });
  }
  
  //Generates an info window with the given content.
  function createInfoWindow(content) {
    if (infoWindow == undefined) {
      infoWindow = new google.maps.InfoWindow({content: content});
    }
    else {
      infoWindow.setContent(content);
    }
    return infoWindow;
  }
  
  //Creates a simple marker with default maps settings. Options should be in the
  //following format {lat: '-22.0', lng: '-44.0', desc: 'Stop description'}
  function createSimpleMarker(options) {
    //TODO Review this to be more generic.
    var marker = new google.maps.Marker({
      map: options.setMap ? map : null,
      position: new google.maps.LatLng(parseFloat(options.lat), parseFloat(options.lng)),
      title: options.desc,
      zIndex: 1
    });
    
    google.maps.event.addListener(marker, 'click', function() {
      if (inEditionMode) {
        descStop = $('<input type="text" value="' + options.desc + '"/>')
        
        saveStop = $('<a href="#save-stop" title="Save Stop">Save Stop</a>');
        saveStop.click(function(){
          //TODO Pass as argument the item removed, added or edited.
          if (opts.callbackMarkerChanged != null) {
            opts.callbackMarkerChanged(convertPositionsToNormalForm(markers), true);
          }
          
          return false;
        });
        
        removeStop = $('<a href="#remove-stop" title="Remove Stop">Remove Stop</a>');
        removeStop.click(function(){
          marker.setMap(null);
          markers.splice(markers.indexOf(marker), 1);
          if (opts.callbackMarkerChanged != null) {
            opts.callbackMarkerChanged(convertPositionsToNormalForm(markers), false);
          }
          return false;
        });
        
        editStopHolder = $('<div class="edit-stop-holder"></div>').append(descStop).append(saveStop).append(removeStop);
        
        infoWindowContent = editStopHolder.get(0);
      } 
      else {
        infoWindowContent = '<p>' + options.desc + '</p>';
      }
      
      createInfoWindow(infoWindowContent);
      infoWindow.open(map, marker);
    });
    
    return marker;
  }
  
  //Create a marker based on the point given and options array. If editable true, this marker
  //will have some listeners assigned to edit the marker.
  function createCustomMarker(point, options, insertAtIndex) {
    
    //If options is undefined it should be set as an empty object to be used on $.extend. 
    if (options == undefined) {
      options = {};
    }
    
    var defaultValues = {
      imageSize: {x: 11, y: 11},
      imageNormalStatePath: 'http://localhost/graviola/web/images/square.png',
      imageHoverStatePath: 'http://localhost/graviola/web/images/square_over.png',
      imageOrigin: {x: 0, y: 0},
      imageAnchor: {x: 6, y: 6}
    };
    defaultValues = $.extend({}, defaultValues, options);
    
    defaultValues.imageSize = new google.maps.Size(defaultValues.imageSize.x, defaultValues.imageSize.y);
    defaultValues.imageOrigin = new google.maps.Point(defaultValues.imageOrigin.x, defaultValues.imageOrigin.x);
    defaultValues.imageAnchor = new google.maps.Point(defaultValues.imageAnchor.x, defaultValues.imageAnchor.x);
    
    imageNormalState = new google.maps.MarkerImage(defaultValues.imageNormalStatePath, defaultValues.imageSize, defaultValues.imageOrigin, defaultValues.imageAnchor);
    imageHoverState = new google.maps.MarkerImage(defaultValues.imageHoverStatePath, defaultValues.imageSize, defaultValues.imageOrigin, defaultValues.imageAnchor);
    
    var marker = new google.maps.Marker({
      position: point,
      map: options.setMap ? map : null,
      icon: imageNormalState,
      draggable: true
    });
    
    //If this is an editable marker, events for edition should be set properly.
    if (inEditionMode) {
      google.maps.event.addListener(marker, "mouseover", function() {
        marker.setIcon(imageHoverState);
      });
      
      google.maps.event.addListener(marker, "mouseout", function() {
        marker.setIcon(imageNormalState);
      });
      
      //Removes a point.
      google.maps.event.addListener(marker, "click", function() {
        for (var index = 0; index < pathMarkers.length; index++) {
          if (pathMarkers[index] == marker) {
            marker.setMap(null);
            pathMarkers.splice(index, 1);
            polyline.getPath().removeAt(index);
            break;
          }
        }
        
        if (opts.callbackPathChanged != null) {
          opts.callbackPathChanged(convertPositionsToNormalForm(pathMarkers), false);
        }
      });
      
      //Change the position of a point.
      google.maps.event.addListener(marker, "drag", function() {
        for (var index = 0; index < pathMarkers.length; index++) {
          if (pathMarkers[index] == marker) {
            polyline.getPath().setAt(index, marker.getPosition());
            break;
          }
        }
        //TODO Not changing the value of array pathMarkers
        if (opts.callbackPathChanged != null) {
          opts.callbackPathChanged(convertPositionsToNormalForm(pathMarkers), true);
        }
      });
      
      
    }
    //TODO Try to get the markers from the map object keep saving these markers in separated
    //arrays is not good.
    if (insertAtIndex == undefined) {
      pathMarkers.push(marker);
    } 
    else {
      pathMarkers.splice(0, 0, marker);
    }
    
    if (opts.callbackPathChanged != null) {
      opts.callbackPathChanged(convertPositionsToNormalForm(pathMarkers), true);
    }
    return marker;
  }
  
  //Remove Polyline and Markers on the map. Basically set the map to null for these so, if the map
  //is set again in these elements they will appear again.
  function clearMap() {
    polyline.setMap(null);
    
    $.each(pathMarkers, function(index, marker){
      marker.setMap(null);
    });
    
    $.each(markers, function(index, marker){
      marker.setMap(null);
    });
  }
  
  //Control callback to enable path edition
  function enablePathEdition() {
    clearMap();
    polyline.setMap(map);
    
    $.each(pathMarkers, function(index, marker){
      marker.setMap(map);
    });
    
    //Click listener to create a new point on the path 
    createPointListener = google.maps.event.addListener(map, 'click', createPoint);
    
    if (createStopListener != undefined) {
      google.maps.event.removeListener(createStopListener);
    }
    
  }
  
  //Control callback to enable stops edition
  function enableStopsEdition() {
    clearMap();
    
    polyline.setMap(map);
    $.each(markers, function(index, marker){
      marker.setMap(map);
    });
    
    //Click listener to create a new stop
    createStopListener = google.maps.event.addListener(map, 'click', createStop);
    
    if (createPointListener != undefined) {
      google.maps.event.removeListener(createPointListener);
    }
    
  }
  
  //Load all additional map controls
  function loadAdditionalMapControls(selector_object) {
    var controls = [];
    if (inEditionMode) {
      //TODO Change the path of the images, these should be more dynamic.
      controls = [{title: 'Route', src: 'http://localhost/graviola/web/images/icons/route.png', clickCallback: enablePathEdition},
                      {title: 'Stop', src: 'http://localhost/graviola/web/images/icons/stop.png', clickCallback: enableStopsEdition}]
                    
    }
    
    $.each(controls, function(index, control){
      map_controler = $("<div class='additional-map-controls'></div>").append("<img src='" + control.src  +"' title='" + control.title  +"'/>");
      //TODO If the editor is already enabled it should not be clickable again.
      $(map_controler).click(function(){
        control.clickCallback(control.clickCallbackParams);
      });
      
      //Convert to format used by google maps API
      map_controler = map_controler.get(0);
      map_controler.index = index;
      map.controls[google.maps.ControlPosition.RIGHT_BOTTOM].push(map_controler);
      
    });
    
  }
  
  $.fn.googlemaps = function(options) {
    //Override the default settings with the values passed on options.
    opts = $.extend({}, $.fn.googlemaps.defaults, options);
    
    return this.each(function(){
      //Initialize maps with settings passed on options otherwise set the default values.
      map = new google.maps.Map($(this).get(0), opts.mapOptions);
      inEditionMode = opts.editable;
      //TODO Handle cases when polyline and stops array are empty
      polyline = createPolyline(opts.routePath);
      
      //When not in edition mode we should set the markers on the map
      createStops(opts.routeStops, !inEditionMode);
      
      map.fitBounds(getBoundsForPath(polyline.getPath()));
      
      //Load additional
      loadAdditionalMapControls(this);
      
    });
  }
  
  //Set default values to init maps. These values will be overwritten in case
  //they're passed to options on $.fn.initMaps.
  $.fn.googlemaps.defaults = 
  {
    mapOptions:
    {
      zoom:       7, 
      mapTypeId:  google.maps.MapTypeId.ROADMAP,
      center:     new google.maps.LatLng(-22.0, -44.0)
    },
    //Set in here one or points for a route. This should be a single array of objects 
    //The object should be in the format {lat: '-22.0', lng: '-44.0'}
    routePath:[],
    routeStops:[],
    //Set whether or not map is on edition on mode.
    editable:  false,
    callbackPathChanged: null,
    callbackMarkerChanged: null
  }
  
  
})(jQuery);